﻿using Bit.Art.Core.Extensions;
using Bit.Art.Core.Models;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Extensions.Logging;

namespace Bit.Art.Distributed
{
    public class Memcache
    {
        private static string s_redisconn;

        private IDatabase Redis
        {
            get
            {
                lock (lockPookRoundRobin)
                {
                    var conn = GetLeastLoadedConnection();
                    return conn.Multiplexer.GetDatabase();
                }
            }
        }

        private static Lazy<Connection>[] lazyConnection = null;

        // TODO: Redis的连接池大小需要可配置
        public static int POOL_SIZE = 100;
        private static readonly Object lockPookRoundRobin = new Object();

        private static Memcache s_memcacheInst;
        private static ILogger _logger;

        // 不允许直接创建
        private Memcache() { }

        // 使用Connection Pool的方式来实现redis对接，以尽量解决timeout异常的问题
        // 参考文档：https://stackoverflow.com/questions/29603890/azure-redis-cache-pool-of-connectionmultiplexer-objects
        // https://dotnetfiddle.net/UCJFeA
        class Connection
        {
            public int TotalOutstanding { get; set; }
            int _id = 0;
            static int count = 0;

            public ConnectionMultiplexer Multiplexer { get; private set; }

            public Connection(ConnectionMultiplexer multiplexer)
            {
                _id = ++count;
                Multiplexer = multiplexer;
            }

            public int Id
            {
                get { return _id; }
            }
        }

        private static Connection GetLeastLoadedConnection()
        {
            Lazy<Connection> lazyContext;

            //choose the least loaded connection from the pool
            var loadedLazys = lazyConnection.Where((lazy) => lazy.IsValueCreated);
            if (loadedLazys.Count() == lazyConnection.Count())
            {
                var minValue = loadedLazys.Min((lazy) => lazy.Value.TotalOutstanding);
                lazyContext = loadedLazys.Where((lazy) => lazy.Value.TotalOutstanding == minValue).First();
            }
            else
            {
                lazyContext = lazyConnection[loadedLazys.Count()];
            }


            lazyContext.Value.TotalOutstanding = lazyContext.Value.TotalOutstanding + 1;
            return lazyContext.Value;
        }

        private static void InitConnectionPool()
        {
            lock (lockPookRoundRobin)
            {
                if (lazyConnection == null)
                {
                    lazyConnection = new Lazy<Connection>[POOL_SIZE];
                }


                for (int i = 0; i < POOL_SIZE; i++)
                {
                    if (lazyConnection[i] == null)
                    {
                        lazyConnection[i] = new Lazy<Connection>(() =>
                        {
                            // _logger.LogWarning($"连接到{s_redisconn}");

                            ConfigurationOptions option = new ConfigurationOptions
                            {
                                AbortOnConnectFail = false,
                                ConnectTimeout=100,
                            };
                            option.EndPoints.Add(s_redisconn);

                            return new Connection(ConnectionMultiplexer.Connect(option));
                        });
                    }
                }
            }
        }

        public static Memcache Create(string connection)
        {
            s_redisconn = connection;
            _logger = ApplicationLogging.LoggerFactory.CreateLogger<Memcache>();
            if (s_memcacheInst == null)
            {
                s_memcacheInst = new Memcache();
                InitConnectionPool();
                // 为了解决redis的timeout异常
                // 参考文档：https://docs.microsoft.com/en-us/azure/redis-cache/cache-how-to-troubleshoot
                // https://blog.marsen.me/2016/11/21/aspdotnet_threadpool_and_redis/
                // 
                var cores = Environment.ProcessorCount;
                var trCounts = 200 / cores;
                ThreadPool.SetMinThreads(trCounts, trCounts);
            }
            return s_memcacheInst;
        }

        public async Task<List<Core.Models.Order>> GetOpenOrders(uint uid)
        {
            var key = string.Format($"oo{uid}");
            return await GetOpenOrders(key);
        }
        
        public async Task<List<Core.Models.Order>> GetOpenOrders(string key)
        {
            var redisValue = await Redis.StringGetAsync(key);

            if (redisValue.HasValue && !redisValue.IsNullOrEmpty)
            {
                var bytes = (byte[])redisValue;
                var orders = Utility.ArrayFromBytes<Core.Models.Order>(bytes);
                return new List<Core.Models.Order>(orders);
            }
            else
            {
                return null;
            }
        }

        public async Task FlushDb()
        {
            await Redis.ExecuteAsync("FLUSHALL");
        }

#if DEBUG
        public async Task<bool> Set(string key, string value)
        {
            return await Redis.StringSetAsync(key, value);
        }
#endif

        public async Task<bool> AddOpenOrder(Core.Models.Order order)
        {
            var uid = order.UserId;
            var key = string.Format($"oo{uid}");
            var existing = await GetOpenOrders(key);
            if (existing == null)
                existing = new List<Core.Models.Order>();

            var idx = existing.BinarySearch(order);
            if (idx < 0)
                existing.Insert(~idx, order);
            else
                existing.Insert(idx, order);
            
            return await Task.FromResult(Redis.StringSet(key, Utility.ToFixedBytes(existing)));
        }

        public async Task<bool> RemoveOpenOrder(uint uid, Guid oid)
        {
            var key = string.Format($"oo{uid}");
            var existing = await GetOpenOrders(key);
            if (existing == null) return false;

            var idx = 0;
            for (; idx < existing.Count; ++idx)
            {
                if (existing[idx].Id == oid)
                    break;
            }

            if (idx < 0)
                return false;
            else if (idx >= 0 && idx < existing.Count)
            {
                var order = existing[idx];
                existing.RemoveAt(idx);
                
                return await Task.FromResult(Redis.StringSet(key, Utility.ToFixedBytes(existing)));
            }
            else
                return false;
        }

        public async Task<bool> CachePermissions(UpdateBalancePermission[] permissions)
        {
            var key = "thirdparty";
            return await Redis.StringSetAsync(key, permissions.ToJson());
        }

        public bool UpdateUserOrderIndexSync(Core.Models.Order order, MemCachedOrderIndex idx)
        {
            var key = string.Format($"uoidx{order.UserId}");
            var value = Redis.StringGet(key);

            List<MemCachedOrderIndex> orders;
            if (value.HasValue && !value.IsNullOrEmpty)
            {
                orders = new List<MemCachedOrderIndex>(Utility.ArrayFromBytes<MemCachedOrderIndex>(value));
            }
            else
            {
                orders = new List<MemCachedOrderIndex>();
            }
            orders.Add(idx);
            return Redis.StringSet(key, Utility.ToFixedBytes(orders));
        }

        public async Task<bool> UpdateUserOrderIndex(Core.Models.Order order, MemCachedOrderIndex idx)
        {
            var key = string.Format($"uoidx{order.UserId}");
            var value = await Task.FromResult(Redis.StringGet(key));

            List<MemCachedOrderIndex> orders;
            if (value.HasValue && !value.IsNullOrEmpty)
            {
                orders = new List<MemCachedOrderIndex>(Utility.ArrayFromBytes<MemCachedOrderIndex>(value));
            }
            else
            {
                orders = new List<MemCachedOrderIndex>();
            }
            orders.Add(idx);
            return await Task.FromResult(Redis.StringSet(key, Utility.ToFixedBytes(orders)));
        }

        public async Task<bool> RemoveUserOrderIndex(Core.Models.Order order)
        {
            var key = string.Format($"uoidx{order.UserId}");
            var value = await Task.FromResult(Redis.StringGet(key));
            List<MemCachedOrderIndex> orders;
            var ret = false;
            if (value.HasValue && !value.IsNullOrEmpty)
            {
                var mcoi = await GetOrderIndex(order.Id);
                if (mcoi == null) return false;

                orders = new List<MemCachedOrderIndex>(Utility.ArrayFromBytes<MemCachedOrderIndex>(value));
                var i = 0;
                for (; i < orders.Count; )
                {
                    if (orders[i].OrderId == mcoi.OrderId)
                        break;

                    ++i;
                }

                var mckey = order.Id.ToByteArray();

                ret = await Redis.KeyDeleteAsync(mckey);

                if (i < orders.Count)
                {
                    orders.RemoveAt(i);
                    return ret && await Task.FromResult(Redis.StringSet(key, Utility.ToFixedBytes(orders)));
                }
            }

            return ret;
        }

        public async Task<MemCachedOrderIndex> GetOrderIndex(Guid orderid)
        {
            var mckey = orderid.ToByteArray();
            var bytes = await Task.FromResult(Redis.StringGet(mckey));
            if (bytes.HasValue && !bytes.IsNullOrEmpty)
            {
                var result = MemCachedOrderIndex.FromBytes(bytes);
                return result;
            }
            else
            {
                return null;
            }
        }

        public bool InsertIndexForDbAndMemCacheSync(Core.Models.Order order, MemCachedOrderIndex idx)
        {
            var mckey = order.Id.ToByteArray();
            return Redis.StringSet(mckey, idx.ToFixedBytes());
        }

        public async Task<bool> InsertIndexForDbAndMemCache(Core.Models.Order order, MemCachedOrderIndex idx)
        {
            var mckey = order.Id.ToByteArray();
            return await Task.FromResult(Redis.StringSet(mckey, idx.ToFixedBytes()));
        }

        public bool CacheUserBalances(uint uid, AccountBalance[] balances)
        {
            var key = string.Format($"ub{uid}");
            var bytes = Utility.ToFixedBytes(balances);
            if (bytes != null)
            {
                return Redis.StringSet(key, bytes);
            }
            else
            {
                return Redis.KeyDelete(key);
            }
        }

        public async Task<bool> UpdateOrAddBalance(uint uid, AccountBalance[] balances, AccountBalance[] savedAllBalances = null)
        {
            if (balances == null) throw new ArgumentNullException(nameof(balances));

            if (savedAllBalances == null)
                savedAllBalances = await GetAllBalances(uid);

            if (savedAllBalances != null)
            {
                var lstAllBalances = new List<AccountBalance>(savedAllBalances);
                foreach (var balance in balances)
                {
                    var idx = Array.BinarySearch(savedAllBalances, balance);
                    if (idx < 0)
                    {
                        lstAllBalances.Insert(~idx, balance);
                    }
                    else
                    {
                        lstAllBalances[idx] = balance;
                    }
                }

                var key = string.Format($"ub{uid}");
                return await Task.FromResult(Redis.StringSet(key, Utility.ToFixedBytes(lstAllBalances)));
            }
            else
            {
                return false;
            }
        }

        public async Task<bool> UpdateOrAddBalance(Core.Models.Order order, bool bid, string quote, string coin,
            decimal dealedQuote, decimal dealedCoin, decimal releasedQuote)
        {
            var uid = order.UserId;
            var deal = order.Volume - order.VolumeRemaining;
            var key = string.Format($"ub{uid}");

            var response = await GetBalances(uid, quote, coin);
            if (response.CoinBalances == null || response.CoinBalances.Length == 0) return false;

            var balances = response.CoinBalances;
            _logger.LogDebug($"balances的长度：{balances.Length}");
            if (bid && order.OrderType == (int)OrderType.Limit)
            {
                _logger.LogInformation($"【买入】tid: {Thread.CurrentThread.ManagedThreadId}, uid: {uid}, 更新余额之前，quote balance: {balances[0].Balance}, quote pending: {balances[0].Pending}, dealedQuote: {dealedQuote}, coin balance: {balances[1].Balance}, dealedCoin: {dealedCoin}");

                balances[0].Balance -= dealedQuote;
                balances[0].Pending -= releasedQuote;
                balances[1].Balance += dealedCoin;

                _logger.LogInformation($"【买入】tid: {Thread.CurrentThread.ManagedThreadId}, uid: {uid}, 更新余额之后，quote balance: {balances[0].Balance}, quote pending: {balances[0].Pending}, coin balance: {balances[1].Balance}");
            }
            else if (!bid && order.OrderType == (int)OrderType.Limit)
            {
                _logger.LogInformation($"【卖出】tid: {Thread.CurrentThread.ManagedThreadId}, uid: {uid}, 更新余额之前，quote balance: {balances[0].Balance}, dealedQuote: {dealedQuote}, coin balance: {balances[1].Balance}, coin pending: {balances[1].Pending}, dealedCoin: {dealedCoin}");

                balances[0].Balance += dealedQuote;
                balances[1].Balance -= dealedCoin;
                balances[1].Pending -= dealedCoin;

                _logger.LogInformation($"【卖出】tid: {Thread.CurrentThread.ManagedThreadId}, uid: {uid}, 更新余额之后，quote balance: {balances[0].Balance}, coin balance: {balances[1].Balance}, coin pending: {balances[1].Pending}");
            }
            else
                throw new NotSupportedException(string.Format($"暂时不支持订单类型：{order.OrderType}"));

            return await UpdateOrAddBalance(uid, balances, response.AllBalances);
        }

        public async Task<bool> ReverseBalance(Core.Models.Order order, string coin, decimal released)
        {
            var uid = order.UserId;
            var key = string.Format($"ub{uid}");
            var response = await GetBalances(uid, coin);
            if (response.CoinBalances == null || response.CoinBalances.Length == 0) return false;

            var balances = response.CoinBalances;
            balances[0].Pending -= released;

            return await UpdateOrAddBalance(uid, balances, response.AllBalances);
        }

        public struct GetBalancesResponse
        {
            public AccountBalance[] AllBalances;

            public AccountBalance[] CoinBalances;
        }

        public async Task<GetBalancesResponse> GetBalances(uint uid, params string[] coins)
        {
            var ret = new GetBalancesResponse();
            var allBalances = await GetAllBalances(uid);
            if (allBalances != null)
            {
                var lst = new List<AccountBalance>();
                foreach (var coin in coins)
                {
                    var idx = Array.BinarySearch(allBalances, coin);
                    if (idx >= 0)
                        lst.Add(allBalances[idx]);
                }

                ret.CoinBalances = lst.ToArray();
            }

            return ret;
        }
        
        private static readonly AccountBalance[] s_EmptyAccountBalances = new AccountBalance[0];
        public async Task<AccountBalance[]> GetAllBalances(uint uid)
        {
            var key = string.Format($"ub{uid}");
            RedisValue value;
            
            value = await Task.FromResult(Redis.StringGet(key));

            if (value.HasValue && !value.IsNullOrEmpty)
            {
                var bytes = (byte[])value;
                _logger.LogDebug($"获取的字节长度：{bytes.Length}");
                return Utility.ArrayFromBytes<AccountBalance>(value);
            }
            else
            {
                return s_EmptyAccountBalances;
            }
        }

        /// <summary>
        /// 获取在缓存在内存中的第三方权限列表
        /// </summary>
        public async Task<UpdateBalancePermission[]> GetThirdPartyPermissions()
        {
            var key = "thirdparty";
            RedisValue value;

            value = await Task.FromResult(Redis.StringGet(key));
            return Utility.From<UpdateBalancePermission[]>(value);
        }
    }
}
